import threading

from engine.dclient import DClient
from engine.sessionmap import remote_session_map
from threading import Thread, Event
from flask import copy_current_request_context
from engine.io import *
from datetime import datetime


class LTimer(Thread):
    """Call a function after a specified number of seconds:

            t = Timer(30.0, f, args=None, kwargs=None)
            t.start()
            t.cancel()     # stop the timer's action if it's still waiting

    """

    def __init__(self, interval, function, args=None, kwargs=None):
        Thread.__init__(self)
        self.interval = interval
        self.function = function
        self.args = args if args is not None else []
        self.kwargs = kwargs if kwargs is not None else {}
        self.finished = Event()

    def cancel(self):
        """Stop the timer if it hasn't finished yet."""
        self.finished.set()

    def run(self):
        while True:
            self.finished.wait(self.interval)
            if not self.finished.is_set():
                self.function(*self.args, **self.kwargs)
            else:
                self.finished.set()
                break


class VModel:
    """
    A virtualized model containing a supported distributed-client list.
    """

    free = '0'
    pending = '1'
    running = '2'
    failed = '3'
    succeed = '4'

    time_format = '%Y-%m-%d %H:%M:%S'

    @staticmethod
    def t2_diff_t1(t1: str, t2: datetime):
        return str((t2 - datetime.strptime(t1, VModel.time_format)).seconds)

    def __init__(self, name: str, d_client_hub: {}):
        self._name = name
        self._state = VModel.free
        self._start_time = ''
        self._elapsed = ''
        self._files = []
        self._log = ''
        self._d_client_hub = d_client_hub
        self._lock = threading.RLock()
        self._active_d_client = None
        self._file_state_info = {}
        #
        # name: _d_client_hub
        # type: dict
        # template:
        # ==========================
        # {
        #     '<remote>': <DClient>,
        #     ...
        # }
        # ==========================
        #

        self._detail = {}
        #
        # name: _detail_info
        # type: dict
        # template:
        # ========================================
        # {
        #     'name': '<model_name>'
        #     'repos_info':
        #     {
        #         '<repo_name>':
        #         {
        #             'name': 'CORE_REPO',
        #             'current':
        #             {
        #                 'full_name': 'dev_sunflower_B012',
        #                 'base_name': 'dev_sunflower_B012',
        #                 'commit_id': '<commit_id>',
        #                 'commit': '<commit>',
        #             }
        #             'remote':
        #             [
        #                 {
        #                     'full_name': 'origin/dev_sunflower_B011',
        #                     'base_name': 'dev_sunflower_B011',
        #                     'commit_id': '<commit_id>',
        #                     'commit': '<commit>',
        #                 },
        #                 {
        #                     'full_name': 'origin/dev_sunflower_B012',
        #                     'base_name': 'dev_sunflower_B012',
        #                     'commit_id': '<commit_id>',
        #                     'commit': '<commit>',
        #                 },
        #                 ...
        #             ]
        #         },
        #         ...
        #     }
        # }
        # ========================================
        #

        self._l_timer = None

    def upload_files(self, upload_data):
        with self._lock:
            io.send(emd_model_start_uploading, upload_data, to=self.sid())

    def update_upload_state_info(self, upload_state_info):
        with self._lock:
            self._file_state_info.update({
                upload_state_info['file_name']: {
                    'file_name': upload_state_info['file_name'],
                    'local_path': upload_state_info['local_path'],
                    'ftp_path': upload_state_info['ftp_path'],
                    'failed_info': upload_state_info['failed_info'],
                    'upload_state': upload_state_info['upload_state'],
                }
            })

    @property
    def name(self):
        return self._name

    @property
    def state(self):
        return self._state

    @property
    def start_time(self):
        return self._start_time

    @property
    def elapsed(self):
        with self._lock:
            if self._state == self.running:
                print('Update.')
                self._update_elapsed()
            print(f'Return elapsed: {self._elapsed}')
            return self._elapsed

    @property
    def files(self):
        return self._files

    @property
    def log(self):
        return self._log

    def _update_elapsed(self):
        self._elapsed = VModel.t2_diff_t1(t1=self._start_time, t2=datetime.now())
        print(f'_update_elapsed to: {self._elapsed}')

    def init_state_running(self):
        with self._lock:
            self._state = VModel.running
            self._start_time = datetime.now().strftime(self.time_format)
            print(f'set {self.name} start time to {self._start_time}')
            self._elapsed = '0'

    def update_state(self, state: str):
        with self._lock:
            self._state = state
            print(f'My start time: {self._start_time}')
            self._update_elapsed()

    def update_files(self, files: []):
        with self._lock:
            self._files = files

    def update_log(self, log):
        with self._lock:
            self._log = log

    def sid(self):
        with self._lock:
            return remote_session_map.get_sid(self._active_d_client.remote)

    def _check_state(self):

        @copy_current_request_context
        def check_state(v_model):
            if v_model._state != VModel.running:
                v_model._l_timer.cancel()
                v_model._l_timer = None
                v_model._elapsed = VModel.t2_diff_t1(t1=v_model._start_time, t2=datetime.now())
                print(f'_update_elapsed to: {self._elapsed}')
                # v_model._elapsed = str((now - datetime.strptime(v_model._start_time, self.time_format)).seconds)
                io.web_send(
                    emw_model_state_report,
                    {
                        'name': v_model._name,
                        'state': v_model._state,
                        'start_time': v_model._start_time,
                        'elapsed': v_model._elapsed,
                        'files': v_model._files,
                        'log': v_model._log
                    },
                )
            else:
                io.send(emd_model_state_request, [v_model._name, ], to=v_model.sid())

        self._l_timer = LTimer(5, check_state, [self, ])
        self._l_timer.start()

    def release(self, settings):
        with self._lock:
            if self._state == self.running:
                return
            if self._l_timer is not None:
                self._l_timer.cancel()
                self._l_timer = None
            c = min(self._d_client_hub.values())
            c.weight += 1
            self._active_d_client = c
            io.send(emd_model_start_releasing, settings, to=remote_session_map.get_sid(c.remote))
            self._state = VModel.running
            self._check_state()

    def add_d_client(self, d_c: DClient):
        with self._lock:
            for _, c in self._d_client_hub.items():
                if c.remote == d_c.remote:
                    return
            self._d_client_hub.update({
                d_c.remote: d_c
            })

    @property
    def d_client_count(self):
        with self._lock:
            return len(self._d_client_hub)

    def remove_d_client(self, remote: str):
        with self._lock:
            for r in list(self._d_client_hub):
                if r == remote:
                    self._d_client_hub.pop(remote)

    def set_detail(self, detail):
        with self._lock:
            self._detail = detail

    def detail_ready(self):
        with self._lock:
            return len(self._detail) > 0

    @property
    def detail(self):
        return self._detail

    def detail_info_empty(self):
        with self._lock:
            return len(self._detail) == 0


class DEngine:
    def __init__(self):
        self._c_lock = threading.RLock()
        self._m_lock = threading.RLock()
        self._d_client_hub = {}
        self._v_model_store = {}
        #
        # _model_store:
        # =============
        # {
        #     '<model_name>': <VModel>,
        #     ...
        # }
        #

    def _update_v_model_store(self, model: str, d_c: DClient):
        if model in self._v_model_store:
            self._v_model_store[model].add_d_client(d_c)
        else:
            self._v_model_store.update({
                model: VModel(model, d_client_hub={d_c.remote: d_c})
            })

    def add_remote(self, remote: str, model_list: []):
        d_c = DClient(remote=remote, model_list=model_list)
        with self._c_lock:
            self._d_client_hub.update({
                remote: d_c
            })
        with self._m_lock:
            for model in model_list:
                self._update_v_model_store(model=model, d_c=d_c)

    def remove_remote(self, remote: str):
        with self._c_lock:
            if remote in self._d_client_hub:
                self._d_client_hub.pop(remote)
                with self._m_lock:
                    for v in list(self._v_model_store):
                        self._v_model_store[v].remove_d_client(remote)
                        if self._v_model_store[v].d_client_count == 0:
                            self._v_model_store.pop(v)

    def model_list(self):
        with self._m_lock:
            return list(self._v_model_store)

    def model_state(self, model):
        with self._m_lock:
            v_m = self._v_model_store.get(model)
            if v_m:
                return {
                    'name': v_m.name,
                    'state': v_m.state,
                    'start_time': v_m.start_time,
                    'elapsed': v_m.elapsed,
                    'files': v_m.files,
                    'log': v_m.log
                }

    def model_state_list(self):
        state_list = []
        with self._m_lock:
            for _, m in self._v_model_store.items():
                state_list.append({
                    'name': m.name,
                    'state': m.state,
                    'start_time': m.start_time,
                    'elapsed': m.elapsed,
                    'files': m.files,
                    'log': m.log
                })
        return state_list

    def _model_detail_info_ready(self, model):
        v_m = self._v_model_store.get(model)
        if v_m:
            return v_m.detail_ready()

    def detail_list_ready(self, model_list):
        with self._m_lock:
            for m in model_list:
                if not self._model_detail_info_ready(m):
                    return False
            return True

    def detail_info_not_ready_list(self, model_list):
        nr_list = []
        with self._m_lock:
            for m in model_list:
                if not self._model_detail_info_ready(m):
                    nr_list.append(m)
        return nr_list

    def model_detail_list(self, model_list):
        """
        :return
        type: dict
        template:
        ========================================
        [
            template=>VModel._detail,
            ...
        ]
        ========================================
        """
        detail_list = []
        with self._m_lock:
            for m in model_list:
                if m in self._v_model_store:
                    detail_list.append(
                        self._v_model_store[m].detail
                    )
        return detail_list

    def update_model_detail_list(
            self, remote, model_detail_list: []):
        with self._m_lock:
            for m in model_detail_list:
                v_m = self._v_model_store.get(m['name'])
                if v_m:
                    v_m.set_detail(m)

    def init_model_state_running(self, model_name):
        print('start init model state...')
        with self._m_lock:
            v_m = self._v_model_store.get(model_name)
            if v_m:
                v_m.init_state_running()

    def update_model_state(self, model_state_list):
        with self._m_lock:
            for m in model_state_list:
                v_m = self._v_model_store.get(m['name'])
                if v_m:
                    v_m.update_files(m['files'])
                    v_m.update_log(m['log'])
                    v_m.update_state(m['state'])

    def client_exists(self, remote: str):
        return remote in self._d_client_hub

    def release(self, models_settings: {}):
        with self._m_lock:
            for m, s in models_settings.items():
                v_m = self._v_model_store.get(m)
                if v_m:
                    v_m.release(s)

    def upload(self, upload_data: []):
        with self._m_lock:
            for data in upload_data:
                v_m = self._v_model_store.get(data['model_name'])
                if v_m:
                    v_m.upload_files(data)

    def update_model_upload_state_info(self, upload_state_info: {}):
        with self._m_lock:
            v_m = self._v_model_store.get(upload_state_info['model_name'])
            if v_m:
                v_m.update_upload_state_info(upload_state_info)
